package nl.ceasaro.poker.ranking;

import nl.ceasaro.playingcards.Card;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by IntelliJ IDEA.
 * User: cees
 * Date: May 14, 2007
 * Time: 9:05:53 PM
 * <p/>
 * This class is responsible to determine the highest possible combination given a
 * set of cards.
 */
public class PokerRanker
{

   public enum PokerRank
   {
      ROYAL_FLUSCH,
      STRAIGHT_FLUSH,
      FOUR_OF_A_KIND,
      FULL_HOUSE,
      FLUSH,
      STRAIGHT,
      THREE_OF_A_KIND,
      TWO_PAIR,
      PAIR,
      HIGH_CARD,
      NONE
   }

   /**
    * check the cards to see if it contains a flush
    * PRE: cards can't contains more then zeven card. Checking for eight or more cards isn't supportted.
    * <br/>
    *
    * @param cards the cards from which to determine the highest rank
    * @return the determined highest rank and five Cards going with this rank.
    */
   public PokerHand getHighestRank(List<Card> cards)
   {
      PokerHand highestHand;
      Collections.sort(cards);
      List<Card> clubs = new ArrayList<Card>();
      List<Card> diamonds = new ArrayList<Card>();
      List<Card> hearts = new ArrayList<Card>();
      List<Card> spades = new ArrayList<Card>();
      List<Card> twos = new ArrayList<Card>();
      List<Card> threes = new ArrayList<Card>();
      List<Card> fours = new ArrayList<Card>();
      List<Card> fives = new ArrayList<Card>();
      List<Card> sixes = new ArrayList<Card>();
      List<Card> sevens = new ArrayList<Card>();
      List<Card> eights = new ArrayList<Card>();
      List<Card> nines = new ArrayList<Card>();
      List<Card> tens = new ArrayList<Card>();
      List<Card> jacks = new ArrayList<Card>();
      List<Card> queens = new ArrayList<Card>();
      List<Card> kings = new ArrayList<Card>();
      List<Card> aces = new ArrayList<Card>();
      //@todo collect the value of the cards right away.
      for (Card card : cards)
      {
         //suits
         if (card.getSuit().equals(Card.Suit.CLUB))
         {
            clubs.add(card);
         }
         if (card.getSuit().equals(Card.Suit.DIAMOND))
         {
            diamonds.add(card);
         }
         if (card.getSuit().equals(Card.Suit.HEART))
         {
            hearts.add(card);
         }
         if (card.getSuit().equals(Card.Suit.SPADE))
         {
            spades.add(card);
         }
         //values
         if (card.getValue().equals(Card.Value.TWO))
         {
            twos.add(card);
         }
         if (card.getValue().equals(Card.Value.THREE))
         {
            threes.add(card);
         }
         if (card.getValue().equals(Card.Value.FOUR))
         {
            fours.add(card);
         }
         if (card.getValue().equals(Card.Value.FIVE))
         {
            fives.add(card);
         }
         if (card.getValue().equals(Card.Value.SIX))
         {
            sixes.add(card);
         }
         if (card.getValue().equals(Card.Value.SEVEN))
         {
            sevens.add(card);
         }
         if (card.getValue().equals(Card.Value.EIGHT))
         {
            eights.add(card);
         }
         if (card.getValue().equals(Card.Value.NINE))
         {
            nines.add(card);
         }
         if (card.getValue().equals(Card.Value.TEN))
         {
            tens.add(card);
         }
         if (card.getValue().equals(Card.Value.JACK))
         {
            jacks.add(card);
         }
         if (card.getValue().equals(Card.Value.QUEEN))
         {
            queens.add(card);
         }
         if (card.getValue().equals(Card.Value.KING))
         {
            kings.add(card);
         }
         if (card.getValue().equals(Card.Value.ACE))
         {
            aces.add(card);
         }
      }
      // check flush and straightFlush
      highestHand = checkFlushOrStraightFlush(diamonds);
      highestHand = highestHand == null ? checkFlushOrStraightFlush(clubs) : highestHand;
      highestHand = highestHand == null ? checkFlushOrStraightFlush(hearts) : highestHand;
      highestHand = highestHand == null ? checkFlushOrStraightFlush(spades) : highestHand;

      highestHand = highestHand == null ? checkStraight(cards) : highestHand;
      // if a flush or straightFlush is found no use to check for other combinations anymore.
      // check four of kind
      highestHand = checkSameOfAKind(aces, cards, highestHand);
      highestHand = checkSameOfAKind(kings, cards, highestHand);
      highestHand = checkSameOfAKind(queens, cards, highestHand);
      highestHand = checkSameOfAKind(jacks, cards, highestHand);
      highestHand = checkSameOfAKind(tens, cards, highestHand);
      highestHand = checkSameOfAKind(nines, cards, highestHand);
      highestHand = checkSameOfAKind(eights, cards, highestHand);
      highestHand = checkSameOfAKind(sevens, cards, highestHand);
      highestHand = checkSameOfAKind(sixes, cards, highestHand);
      highestHand = checkSameOfAKind(fives, cards, highestHand);
      highestHand = checkSameOfAKind(fours, cards, highestHand);
      highestHand = checkSameOfAKind(threes, cards, highestHand);
      highestHand = checkSameOfAKind(twos, cards, highestHand);

      highestHand = highestHand == null ? getHighestCards(cards, 5) : highestHand;
      return highestHand;
   }

   /**
    * @param suitedCards the cards, they MUST be all of the same suit.
    * @return the PokerHand with a flush or straigh flush.
    *         If no flushes are found <i>null</i> is returned.
    */
   private PokerHand checkFlushOrStraightFlush(List<Card> suitedCards)
   {
      PokerHand pokerHand = null;
      if (suitedCards.size() >= 5)
      {
         PokerHand straightFlush = checkStraight(suitedCards);
         if (straightFlush != null)
         {
            pokerHand = straightFlush;
            pokerHand.setRank(PokerRank.STRAIGHT_FLUSH);
         }
         else if (suitedCards.size() > 5)
         {
            pokerHand = getHighestCards(suitedCards, 5);
            pokerHand.setRank(PokerRank.FLUSH);
         }
         else
         {
            pokerHand = new PokerHand(suitedCards, PokerRank.FLUSH);
         }
      }
      return pokerHand;
   }

   /**
    * Analysis the cards and looks for a straight. If five cards in sequence are found they are returned.
    * This method doesn't check if the cards are of the same suit.
    *
    * @param cards the cards to check
    * @return a pokerHand with the straight if found, otherwise <i>null</i> is returned.
    */
   private PokerHand checkStraight(List<Card> cards)
   {
      PokerHand pokerHand = null;
      ArrayList<Card> straight = PokerRankCalculator.getStraight(cards, false);
      if (straight.size() == 5)
      {
         pokerHand = new PokerHand(straight, PokerRanker.PokerRank.STRAIGHT);
      }
/*
      int offset = 0;
      while (cards.size() - offset >= 5 && pokerHand == null)
      {
         Collections.sort(cards);
         if (cards.get(offset).getValue().ordinal() == cards.get(offset + 1).getValue().ordinal() - 1 &&
                 cards.get(offset).getValue().ordinal() == cards.get(offset + 2).getValue().ordinal() - 2 &&
                 cards.get(offset).getValue().ordinal() == cards.get(offset + 3).getValue().ordinal() - 3 &&
                 cards.get(offset).getValue().ordinal() == cards.get(offset + 4).getValue().ordinal() - 4
                 )
         {
            ArrayList<Card> straight = new ArrayList<Card>();
            straight.add(cards.get(offset));
            straight.add(cards.get(offset + 1));
            straight.add(cards.get(offset + 2));
            straight.add(cards.get(offset + 3));
            straight.add(cards.get(offset + 4));
            pokerHand = new PokerHand(straight, PokerRank.STRAIGHT);
         }
         offset++;
      }
      if (cards.size() >= 5)
      {
         if (cards.get(0).getValue() == Card.Value.ACE &&
                 cards.get(1).getValue() == null)
         {

         }
      }
*/
      return pokerHand;
   }

   /**
    * @param cardsWithSameValue the cards, they MUST be all of the same value.
    * @param allCards           all the cards used to determine the hand.
    * @param pokerHand          the pokerHand already found.
    * @return the PokerHand with the quads (four of a kind).
    *         If there isn't a quads <i>null</i> is returned.
    */
   private PokerHand checkSameOfAKind(List<Card> cardsWithSameValue, List<Card> allCards, PokerHand pokerHand)
   {
      if (cardsWithSameValue.size() == 4)
      {
         pokerHand = new PokerHand(cardsWithSameValue, PokerRank.FOUR_OF_A_KIND);
         Card highCard = getHighCard(allCards, cardsWithSameValue);
         pokerHand.setKicker(highCard);
         return pokerHand;
      }
      if (cardsWithSameValue.size() == 3)
      {
         // check for full house
         if (pokerHand != null)
         {
            if (pokerHand.getRank() == PokerRank.THREE_OF_A_KIND)
            {
               Card combiCard = pokerHand.getCombinationCards().get(0);
               Card checkingCard = cardsWithSameValue.get(0);
               //if (combiCard.getValue().ordinal() > checkingCard.getValue().ordinal())
               if (combiCard.isHigher(checkingCard))
               {
                  // use three cards of the combinationCards and two of the cardsWithSameValue to make a full house
                  cardsWithSameValue.add(pokerHand.getCombinationCards().get(0)); // add the full house cards.
                  cardsWithSameValue.add(pokerHand.getCombinationCards().get(1)); // add the full house cards.
                  pokerHand.setCombinationCards(cardsWithSameValue);
               }
               else
               {
                  pokerHand.getCombinationCards().add(cardsWithSameValue.get(0)); // add the full house cards.
                  pokerHand.getCombinationCards().add(cardsWithSameValue.get(1)); // add the full house cards.
                  // use three cards of the cardsWithSameValue and two of the combinationCards to make a full house
               }
               pokerHand.clearKickers();
               pokerHand.setRank(PokerRank.FULL_HOUSE);
            }
            else if (pokerHand.getRank() == PokerRank.TWO_PAIR)
            {
               List<Card> combiCards = pokerHand.getCombinationCards();
               List<Card> newCombiCards = new ArrayList<Card>();
               Card highestCard = getHighCard(combiCards);
               for (Card combiCard : combiCards)
               {
                  if (combiCard.getValue() == highestCard.getValue())
                  {
                     newCombiCards.add(combiCard);
                  }
               }
               newCombiCards.addAll(cardsWithSameValue);
               pokerHand.setCombinationCards(newCombiCards);
               pokerHand.clearKickers();
               pokerHand.setRank(PokerRank.FULL_HOUSE);
            }
            else if (pokerHand.getRank() == PokerRank.PAIR)
            {
               pokerHand.getCombinationCards().addAll(cardsWithSameValue); // add the full house cards.
               pokerHand.clearKickers();
               pokerHand.setRank(PokerRank.FULL_HOUSE);
            }
         }
         else
         {
            pokerHand = new PokerHand(cardsWithSameValue, PokerRank.THREE_OF_A_KIND);
            Card kicker = getHighCard(allCards, cardsWithSameValue);
            ArrayList<Card> cardsToExclude = new ArrayList<Card>();
            cardsToExclude.addAll(cardsWithSameValue);
            cardsToExclude.add(kicker);
            Card secondKicker = getHighCard(allCards, cardsToExclude);
            pokerHand.setKicker(kicker);
            pokerHand.setSecondKicker(secondKicker);
         }
      }
      if (cardsWithSameValue.size() == 2)
      {
         if (pokerHand != null)
         {
            if (pokerHand.getRank() == PokerRank.THREE_OF_A_KIND)
            {
               pokerHand.getCombinationCards().addAll(cardsWithSameValue); // add the full house cards.
               pokerHand.clearKickers();
               pokerHand.setRank(PokerRank.FULL_HOUSE);
            }
            else if (pokerHand.getRank() == PokerRank.TWO_PAIR)
            {
               List<Card> cards = pokerHand.getCombinationCards();
               Card lowCard = getLowCard(cards);
               Card checkingCard = cardsWithSameValue.get(0);
               if (lowCard.isHigher(checkingCard))
               {
                  List<Card> newCombiCards = new ArrayList<Card>();
                  List<Card> combiCards = pokerHand.getCombinationCards();
                  for (Card combiCard : combiCards)
                  {
                     if (combiCard.getValue() != lowCard.getValue())
                     {
                        newCombiCards.add(combiCard); // only add the higher pair of the already found two pair
                     }
                  }
                  newCombiCards.addAll(cardsWithSameValue); // add the new higher pair.
                  pokerHand.setCombinationCards(newCombiCards);
               }
               pokerHand.clearKickers();
               Card kicker = getHighCard(allCards, pokerHand.getHand());
               pokerHand.setKicker(kicker);
            }
            else if (pokerHand.getRank() == PokerRank.PAIR)
            {
               pokerHand.getCombinationCards().addAll(cardsWithSameValue); // add the second pair.
               pokerHand.clearKickers();
               Card kicker = getHighCard(allCards, cardsWithSameValue);
               pokerHand.setKicker(kicker);
               pokerHand.setRank(PokerRank.TWO_PAIR);
            }
         }
         else
         {
            pokerHand = new PokerHand(cardsWithSameValue, PokerRank.PAIR);
            Card kicker = getHighCard(allCards, cardsWithSameValue);
            pokerHand.setKicker(kicker);
            Card secondKicker = getHighCard(allCards, pokerHand.getHand());
            pokerHand.setSecondKicker(secondKicker);
            Card thirdKicker = getHighCard(allCards, pokerHand.getHand());
            pokerHand.setThirdKicker(thirdKicker);
         }
      }
      return pokerHand;
   }

   /**
    * @param cards  the cards to check
    * @param number the number of highest cards to return.
    * @return the specified number of highest cards of the specified cards.
    *         e.g. <code>getHighestCards([6D, JS, AS, 4H, 7H, 3H, 10H], 5)</code> returns
    *         <code>[AS, JS, 10H 7H, 6D]</code>.<br/>
    *         If the number is higher than the amount of cards in the card list, <i>null</i>
    *         is returned.
    */
   private PokerHand getHighestCards(List<Card> cards, int number)
   {
      if (cards.size() < number)
      {
         return null;
      }
      Collections.sort(cards);
      List<Card> highestFiveCards = new ArrayList<Card>();
      for (int i = 0; i < number; i++)
      {
         highestFiveCards.add(cards.get(i));
      }
      return new PokerHand(highestFiveCards, PokerRank.HIGH_CARD);
   }

   /**
    * @param allCards the cards search for the highest values
    * @return the highest card found in allCards
    */
   private Card getHighCard(List<Card> allCards)
   {
      return getHighCard(allCards, new ArrayList<Card>());
   }

   /**
    * @param allCards     the cards search for the highest values
    * @param excludedCard card to be excluded from allCards
    * @return the highest card found in allCards
    * @noinspection UnusedDeclaration
    */
   private Card getHighCard(List<Card> allCards, Card excludedCard)
   {
      ArrayList<Card> excludedCards = new ArrayList<Card>();
      excludedCards.add(excludedCard);
      return getHighCard(allCards, excludedCards);
   }

   /**
    * @param allCards      the cards search for the highest values
    * @param excludedCards cards to be excluded from allCards
    * @return the highest card found in allCards
    */
   private Card getHighCard(List<Card> allCards, List<Card> excludedCards)
   {
      Card highestCard = null;
      for (Card card : allCards)
      {
         if (!excludedCards.contains(card))
         {
            if (highestCard == null)
            {
               highestCard = card;
            }
            else
            {
               if (highestCard.isHigher(card))
               {
                  highestCard = card;
               }
            }
         }
      }
      return highestCard;
   }

   /**
    * @param allCards the cards search for the lowest values
    * @return the lowest card found in allCards
    */
   private Card getLowCard(List<Card> allCards)
   {
      Card lowestCard = null;
      for (Card card : allCards)
      {
         if (lowestCard == null)
         {
            lowestCard = card;
         }
         else
         {
            if (lowestCard.isLower(card))
            {
               lowestCard = card;
            }
         }
      }
      return lowestCard;
   }

}

